iT邦幫忙

2021 iThome 鐵人賽

DAY 27
2
Software Development

從 JavaScript 角度學 Python系列 第 27

從 JavaScript 角度學 Python(27) - 傳值?傳參考?

  • 分享至 

  • xImage
  •  

前言

已經快到鐵人賽的結尾了,但是我現在才想到我好像少講了一個東西,就是關於傳值與傳參考的部分,所以這一篇就來聊一下關於傳值與傳參考吧。

傳值

首先先簡單聊一下純粹的傳值,在 JavaScript 中我們知道原始型別是單純傳遞一個值給另一個變數,因此修改時並不會影響原有的變數:

var a = 'Ray';
var b = a;
b = 'QQ';

console.log(a, b); // Ray, QQ

那話題拉回到 Python 中,Python 與 JavaScript 也是相同的嗎?我們可以直接來看一次範例:

a = 'Ray'
b = a
b = 'QQ'

print(a, b) # Ray, QQ

從結果來看,我們可以看到 Python 與 JavaScript 的結果是一樣的。

但是如果今天採用的是另一種方式來呈現的話結果會相同嗎?這邊就讓我們來看看另一種範例程式碼:

def fn(x, y):
  cache = 100
  x = cache
  y = x

x = 10
y = 20

fn(x, y)

print(x, y) # 10, 20

我們可以看到在上面的範例程式碼中,我將全域的變數 xy 傳入到 fn 函式內,接下來在 fn 中宣告一個 cache = 100 的變數,而後面我做了重新賦予 xy 的行為,但是你會發現 fn 內的調整並不會影響全域變數的結果,這代表著什麼呢?代表著我們只是傳遞值 (x, y) 給 fn 函式的參數使用,因此函式內部的變化它並不會影響原本的變數。

或許這個時候你會很聰明的想到我在「從 JavaScript 角度學 Python(6) - 變數作用域」章節中有介紹一個「global」的方式可以針對外部變數做一些ㄌ調整,但是基本上如果你在這邊使用 global 方法的話是會出現錯誤的:

def fn(x, y):
  cache = 100
  global x, y # SyntaxError: name 'x, y' is parameter and global
  x = cache
  y = x
  

x = 10
y = 20

fn(x, y)

print(x, y) # 10, 20

而會出現「SyntaxError: name 'x, y' is parameter and global」這一段錯誤訊息的主要原因在於,函式的參數如果與 global 指定的變數相同的話,是會無法正常運作的,因為它會不知道你到底是要使用哪一個參數還是變數唷~

你不能這樣玩唷~

所以我們這邊可以大膽的推測...

「或許 Python 的數值型別、字串型別與布林型別會與 JavaScript 的原始型別類似?」

那麼關於這個推測我們可以試著實踐一次或許會更精準,透過前面幾個章節我們了解到容器型別類似於 JavaScript 的陣列與物件,所以這邊我們可以試著撰寫字串、布林與數值型別來測試看看:

def fn(w, x, y, z):
  w = False
  x = 10
  y = 1.1
  z = 'Ray'
  print(w, x, y, z) # False, 10, 1.1, Ray


w = True # 布林
x = 9.9 # 浮點數
y = 'Ray' # 字串
z = 10 # 整數


fn(w, x, y, z)

print(w, x, y, z) # True, 9.9, Ray, 10

透過上面的結果來看,基本上 Python 的字串、布林與數值型別確實是與 JavaScript 的基本型別雷同,那字典與串列呢?別急,我們先接著下去看,但是這邊要注意我們是在講 Python 不是 JavaScript:

https://ithelp.ithome.com.tw/upload/images/20210926/20119486vFhemZF5Vo.png

傳參考

接下來聊一下剛剛沒有提到的字典與串列的部分,雖然我們知道 Python 的字典與串列就是對應著 JavaScript 的物件與陣列,但實際上運作模式會是一樣嗎?所以這邊也來快速聊一下這一塊。

首先我們先來一段 JavaScript 很常見的物件傳參考的基本範例程式碼:

var b = {
  name: 'Ray',
};

var e = b;

e.name = 'QQ';
console.log(b.name); // QQ

上面的觀念我們都知道這是一個物件傳參考概念,如果對於這一段有興趣的話可以閱讀我這一篇文章「JavaScript 核心觀念(27)-物件-物件參考觀念的實際運作模式」會有更詳細的說明,因此這邊就不著墨了(笑)。

https://ithelp.ithome.com.tw/upload/images/20210926/20119486DoXEZZwLHO.png

前面講那麼多,那 Python 的狀況又如何呢?我們將上面的 JavaScript 範例程式碼直接改寫成 Python 版本試試看:

b = {
  'name': 'Ray'
}

e = b

e['name'] = 'QQ'
print(b['name']) # QQ

好吧...依照輸出的結果來看, Python 的字典跟 JavaScript 的物件傳參考狀況非常像沒有錯吧~

接下來讓我們看看另一個範例程式碼:

def fn (x):
  x['name'] = 'QQ'

b = {
  'name': 'Ray'
}

fn(b)

print(b['name']) # QQ

上面這個範例程式碼確實沒有任何問題,感覺也是物件傳參考有非常大的關聯性,但是如果範例程式碼變成了以下,那麼結果會是怎樣呢?

def fn (x):
  x = {
    'name': 'Ray'
  }


b = {
  'name': 'Ray'
}

fn(b)

print(b['name']) # Ray

疑?奇怪了,為什麼結果不一樣了?

https://ithelp.ithome.com.tw/upload/images/20210926/20119486AoXN9Ah6tB.png

好吧,這邊要講一下關於物件(字典)傳參考的時候會有一個很特別的地方,雖然我們知道 Python 的字典基本上與 JavaScript 非常雷同,因此再將字典賦予到變數時,其實是賦予一個記憶體空間位置,我知道這邊有點難懂,所以我想換個方式形容,我們試著把容器型別與數值型別、字串型別以生活化的角度去理解看看。

而這邊會用車子去當作舉例,因此數值型別與字串型別你可以把它想像成是車子內的某些東西,例如:輪胎、方向盤,又或者收音機等等,字典與串列的話則是一個僅有車殼的車子(連引擎都沒有),所以透過這樣的舉例來講,可能就會像這樣:

# 把字典看成一個車殼,裡面放著車子的東西
car = {
  'steeringWheel': 1, # 方向盤
  'wheels': 4, # 輪胎
  'radio': 1, # 收音機
}

那麼當我將 car 字典賦予到另一個變數時,其實概念類似我把車子借給他人使用,例如我借來了一台車:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car

所以當我對著車子有任何調整,可能拆掉一個輪胎,那就會影響到原本的車子,畢竟車子是借來的:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car
ray['wheels'] = 3

print(car['wheels']) # 3

那出借東西的行為就是 JavaScript 的物件傳參考,我只是借給你用而已的概念。

那麼有一種狀況下我們不會影響到原有的車子,也就是我後來終於有錢買自己的車子:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car 

ray = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

因為後來自己買了一台車子的關係,所以我就不再跟人家借車子,所以我接下來不管怎麼調整自己的車子都不會影響到原本 car 變數:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car 

ray = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray['radio'] = 0

print(car['radio']) # 1

上面的舉例只是希望讀者在閱讀時可以比較好理解,因此只要你看到變數被重新賦予一個字典 or 物件,就代表他會重新指向到一個新的記憶體空間(空車殼),如果這樣子還不好理解的話,以後你只要看到變數被重新賦予一個 {} or [] 就代表著它被重新指定了位址,因此就不會發生所謂的物件傳參考問題。

當然你也可以活用前面章節所學的 id 來查看彼此的記憶體空間是否相同:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car

print(id(ray), id(car)) # 4463682048, 4463682048

如果是重新賦予的話,則是不同的記億體位置:

car = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

ray = car 

ray = {
  'steeringWheel': 1,
  'wheels': 4,
  'radio': 1,
}

print(id(ray), id(car)) # 4418249408 4418249216

透過前面章節所學的某些技巧也可以幫助到你理解,因此前面章節所分享的觀念都是非常重要的唷~

https://ithelp.ithome.com.tw/upload/images/20210926/2011948637Ca1ifb9U.png

call by xxx

最後其實你應該會發現我一直刻意閃過一些東西不講,例如...call by xxx or pass by xxx 什麼的,最主要原因是我自己對於 Python 沒有非常的熟悉,所以自己也不敢多講什麼,主要也是怕講錯。

但是如果真的硬要問我的話,我可能會依據 wiki 所說的來講,因為以 wiki 的描述來講,Python 是比較接近 Call by sharing

傳共享物件呼叫(Call by sharing)的方式由Barbara Liskov命名,並被Python、Java(物件類型)、JavaScript、Scheme、OCaml等語言使用。

而這一點也剛好跟 JavaScript 非常像,因此我自己覺得你想把 Python 看成 JavaScript 似乎也可以?!

最後的最後也想聊個好玩的東西,也就是以下程式碼:

a = 'Ray'
b = 'Ray'

print('a', a, id(a)) # a Ray 4351631984
print('b', b, id(b)) # b Ray 4351631984

上面你可以發現我並沒有 a = b,但是兩者的記憶體位置卻是相同,因此代表著字串在建立時都是指向同一個記憶體位置,也因此才會導致 a == b 是一個 True,而這一段我覺得可以理解下面這張圖:

https://ithelp.ithome.com.tw/upload/images/20210926/20119486RdjhCdaFkf.png

但是如果重新賦予其他值的時候,就會是直接指向到另一個記憶體位置:

https://ithelp.ithome.com.tw/upload/images/20210926/20119486juUb2kjkPu.png

如果是字典的話,那麼也是類似,但是字典則是看到一個新的 {} or [] 才會重新指向,這邊就讓我偷懶一下用之前 「JavaScript 核心觀念(27)-物件-物件參考觀念的實際運作模式」的文章圖片偷懶一下:

https://ithelp.ithome.com.tw/upload/images/20210926/20119486hwVXnU1WZY.png

那今天也差不多介紹到這邊就告一個段落囉~

參考文獻

作者的話

中秋節那段時間因為回老家團圓的關係,所以訊號一直都很差,幾乎只能打電話不能上網的等級,所以中秋節的晚上我都只能站在馬路正中央收發訊息,收發完就要快點閃離馬路,不然我現在應該不會在這邊寫文章了...

關於兔兔們

兔法無邊


上一篇
從 JavaScript 角度學 Python(26) - 指定直譯器
下一篇
從 JavaScript 角度學 Python(28) - 閉包(Closure)
系列文
從 JavaScript 角度學 Python31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言